ECS Fargateのデプロイ環境をCFnでサクッと構築してみた(Rollingアップデート編)
こんにちは。AWS事業本部トクヤマシュンです。
ECS Fargateのデプロイ方法には、RollingアップデートとBlue/Greenデプロイメントの2種類があります。
それぞれの違いはドキュメントに記載がありますが、実際に触ってみないと中々理解しづらいものです。
そこで今回はCloudFormationを使って両方のデプロイ環境をサクっと構築するためのテンプレートをご紹介します。
長くなってしまったため、本エントリではRollingアップデートを紹介します。
興味のある方は、是非一度ご自身の環境で試してみてください。
Blue/Greenデプロイメントは次のエントリに記載があるので、興味のある方は確認してみてください。
構築する環境
今回の構成は次の図の通りです。
CFnテンプレートなどのソースコード
構築用のCFnテンプレートやパイプラインソースとして利用するDockerファイルを下記GitHubに格納していますので、構築の際はご参考ください。
CFn構築の際はCLIを使うことをオススメします。
parameters/
配下に変数ファイルを準備していますので、各自の環境に合わせて適宜設定してください。
リソース名はプレフィックスをSystemName-Environmentで統一しています。
デフォルトでは、SystemName=system, Environment=devとしています。
次章からはCFnテンプレートを確認します。
CFnテンプレートは折りたたんで記載してありますので、ご注意ください
VPC構築
VPCスタック作成
次のCFnテンプレートを使ってスタックを作成し、VPCを構築します。
(VPCはRollingアップデート、Blue/Greenデプロイメント共通です)
vpc.yaml(クリックで展開)
AWSTemplateFormatVersion: 2010-09-09 Description: VPC Create Metadata: "AWS::CloudFormation::Interface": ParameterGroups: - Label: default: "System Configuration" Parameters: - Environment - SystemtName - Label: default: "Netowork Configuration" Parameters: - VPCCIDR - AvailabilityZone1 - AvailabilityZone2 - PublicSubnet1CIDR - PublicSubnet2CIDR - PrivateSubnet1CIDR - PrivateSubnet2CIDR # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: Environment: Default: dev Type: String SystemName: Default: system Type: String VPCCIDR: Type: String Default: "10.10.0.0/16" AvailabilityZone1: Type: String Default: "ap-northeast-1a" AvailabilityZone2: Type: String Default: "ap-northeast-1c" PublicSubnet1CIDR: Type: String Default: "10.10.0.0/24" PublicSubnet2CIDR: Type: String Default: "10.10.1.0/24" PrivateSubnet1CIDR: Type: String Default: "10.10.2.0/24" PrivateSubnet2CIDR: Type: String Default: "10.10.3.0/24" Resources: # ------------------------------------------------------------# # VPC # ------------------------------------------------------------# VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VPCCIDR EnableDnsSupport: 'true' EnableDnsHostnames: 'true' InstanceTenancy: default Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-vpc" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Internet Gateway # ------------------------------------------------------------# IGW: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-igw" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref IGW # ------------------------------------------------------------# # NAT Gateway # ------------------------------------------------------------# NGW1: Type: "AWS::EC2::NatGateway" Properties: AllocationId: !GetAtt NGW1EIP.AllocationId SubnetId: !Ref PublicSubnet1 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-ngw-1" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" NGW1EIP: Type: "AWS::EC2::EIP" Properties: Domain: vpc Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-nat-eip-1" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" NGW2: Type: "AWS::EC2::NatGateway" Properties: AllocationId: !GetAtt NGW2EIP.AllocationId SubnetId: !Ref PublicSubnet2 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-ngw-2" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" NGW2EIP: Type: "AWS::EC2::EIP" Properties: Domain: vpc Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-nat-eip-2" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # PublicSubnet # ------------------------------------------------------------# PublicSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: !Ref PublicSubnet1CIDR AvailabilityZone: !Ref AvailabilityZone1 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-public-subnet-1" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" PublicSubnet2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: !Ref PublicSubnet2CIDR AvailabilityZone: !Ref AvailabilityZone2 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-public-subnet-2" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Public RouteTable # ------------------------------------------------------------# PublicRouteTable1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-public-rt-1" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" PublicRouteTable2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-public-rt-2" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Public Routing # ------------------------------------------------------------# PublicRoute1: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PublicRouteTable1 DestinationCidrBlock: "0.0.0.0/0" GatewayId: !Ref IGW PublicRoute2: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PublicRouteTable2 DestinationCidrBlock: "0.0.0.0/0" GatewayId: !Ref IGW # ------------------------------------------------------------# # Public RouteTable Association # ------------------------------------------------------------# PublicSubnetAttach1: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable1 SubnetId: !Ref PublicSubnet1 PublicSubnetAttach2: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable2 SubnetId: !Ref PublicSubnet2 # ------------------------------------------------------------# # PrivateSubnet # ------------------------------------------------------------# PrivateSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: !Ref PrivateSubnet1CIDR AvailabilityZone: !Ref AvailabilityZone1 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-private-subnet-1" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" PrivateSubnet2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: !Ref PrivateSubnet2CIDR AvailabilityZone: !Ref AvailabilityZone2 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-private-subnet-2" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Private RouteTable # ------------------------------------------------------------# PrivateRouteTable1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-private-rt-1" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" PrivateRouteTable2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-private-rt-2" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Private Routing # ------------------------------------------------------------# PrivateRoute1: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable1 DestinationCidrBlock: "0.0.0.0/0" NatGatewayId: !Ref NGW1 PrivateRoute2: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable2 DestinationCidrBlock: "0.0.0.0/0" NatGatewayId: !Ref NGW2 # ------------------------------------------------------------# # Private RouteTable Association # ------------------------------------------------------------# PrivateSubnetAttach1: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 PrivateSubnetAttach2: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTable2 SubnetId: !Ref PrivateSubnet2
VPC構築のポイント
- private subnetからはNAT Gatewayを通じてインターネット経由でECS・ECRにアクセスする想定
- ビルドやECSタスクの更新を頻繁に行う環境であれば、NAT Gatewayの転送料金を防ぐためにVPCエンドポイント経由とするなどの対策が必要
- 参考:そのトラフィック、NATゲートウェイを通す必要ありますか?適切な経路で不要なデータ処理料金は削減しましょう
ECR構築
Rollingアップデート用ECRスタック作成
次のCFnテンプレートを使ってスタックを作成し、Rollingアップデート用のECRを構築します。
ecr-rolling.yaml(クリックで展開)
AWSTemplateFormatVersion: 2010-09-09 Description: ECR Create Metadata: "AWS::CloudFormation::Interface": ParameterGroups: - Label: default: "System Configuration" Parameters: - Environment - SystemtName # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: Environment: Default: dev Type: String SystemName: Default: system Type: String Resources: # ------------------------------------------------------------# # ECR # ------------------------------------------------------------# RollingECR: Type: AWS::ECR::Repository Properties: RepositoryName: !Sub "${SystemName}-${Environment}-rolling-repo" EncryptionConfiguration: EncryptionType: "AES256" ImageScanningConfiguration: ScanOnPush: true ImageTagMutability: IMMUTABLE Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-repo" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}"
ECR構築のポイント
- リポジトリはSSE-S3(AES256)によるサーバー側暗号化を実施
- push時にイメージスキャンを行う
- タグはIMMUTABLEとする
ECS構築
Rolling アップデート用ECS
次のCFnテンプレートを使ってスタックを作成し、Rollingアップデート用のECSを構築します。
ecs-rolling.yaml
AWSTemplateFormatVersion: 2010-09-09 Description: ECS and ALB Create Metadata: "AWS::CloudFormation::Interface": ParameterGroups: - Label: default: "System Configuration" Parameters: - Environment - SystemtName - Label: default: "Netowork Configuration" Parameters: - VpcId - ALBSubnetId1 - ALBSubnetId2 - ECSSubnetId1 - ECSSubnetId2 - ALBAllowInboundIP - Label: default: "Fargate Configuration" Parameters: - ECSImage - ECSTaskCPUUnit - ECSTaskMemory - ECSTaskDesiredCount - Label: default: "Scaling Configuration" Parameters: - ServiceScaleCpuTarget - ServiceScaleInCooldown - ServiceScaleOutCooldown - TaskMinContainerCount - TaskMaxContainerCount # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: Environment: Default: dev Type: String SystemName: Default: system Type: String #VPCID VpcId: Description : "VPC ID" Type: AWS::EC2::VPC::Id Default: "vpc-" #ALBSubnet1 ALBSubnetId1: Description : "ALB Subnet 1st" Type : AWS::EC2::Subnet::Id Default: "subnet-public1Id" #ALBSubnet2 ALBSubnetId2: Description : "ALB Subnet 2nd" Type : AWS::EC2::Subnet::Id Default: "subnet-public2Id" #ALBAllowInboundIpAddress ALBAllowInboundIP: Description : "ALB Subnet 2nd" Type: String Default: "xxx.xxx.xxx.xxx/32" #ECSSubnet1 ECSSubnetId1: Description : "ECS Subnet 1st" Type : AWS::EC2::Subnet::Id Default: "subnet-private1Id" #ECSSubnet2 ECSSubnetId2: Description : "ECS Subnet 2nd" Type : AWS::EC2::Subnet::Id Default: "subnet-private2Id" #ECSTaskCPUUnit ECSTaskCPUUnit: Type: String Default: "256" #ECSTaskMemory ECSTaskMemory: Type: String Default: "512" #ECSImage ECSImage: Type: String Default: "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/system-dev-rolling-repo:latest" #ECSTaskDesiredCount ECSTaskDesiredCount: Type: Number Default: 1 # Scaling params ServiceScaleCpuTarget: Description: Target Tracking Scaling CPU Target Type: Number Default: 70 ServiceScaleInCooldown: Type: Number Description: Target Tracking Scale In Cooldown seconds Default: 180 ServiceScaleOutCooldown: Type: Number Description: Target Tracking Scale Out Cooldown seconds Default: 60 TaskMinContainerCount: Type: Number Description: Minimum number of containers to run for the service Default: 1 ConstraintDescription: Value must be at least one TaskMaxContainerCount: Type: Number Description: Maximum number of containers to run for the service when auto scaling out Default: 2 ConstraintDescription: Value must be at least one Resources: # ------------------------------------------------------------# # Security Group # ------------------------------------------------------------# ALBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: ALB Security Group GroupName: !Sub "${SystemName}-${Environment}-rolling-alb-sg" VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: !Ref ALBAllowInboundIP Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-alb-sg" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" TaskSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Task Security Group GroupName: !Sub "${SystemName}-${Environment}-rolling-task-sg" VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: !Ref ALBSecurityGroup SecurityGroupEgress: - IpProtocol: -1 CidrIp: 0.0.0.0/0 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-task-sg" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # ALB # ------------------------------------------------------------# ALB: Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" Properties: Name: !Sub "${SystemName}-${Environment}-rolling-alb" LoadBalancerAttributes: - Key: "deletion_protection.enabled" Value: false - Key: "idle_timeout.timeout_seconds" Value: 60 - Key: "access_logs.s3.enabled" Value: true - Key: "access_logs.s3.bucket" Value: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}" Scheme: "internet-facing" SecurityGroups: - !Ref ALBSecurityGroup Subnets: - !Ref ALBSubnetId1 - !Ref ALBSubnetId2 Type: "application" Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-alb" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" ALBListener: Type: "AWS::ElasticLoadBalancingV2::Listener" Properties: DefaultActions: - TargetGroupArn: !Ref TargetGroup Type: forward LoadBalancerArn: !Ref ALB Port: 80 Protocol: HTTP TargetGroup: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: HealthCheckPath: / VpcId: !Ref VpcId Name: !Sub "${SystemName}-${Environment}-rolling-tg" Protocol: HTTP Port: 80 TargetType: ip # ------------------------------------------------------------# # ALB Log S3 Bucket # ------------------------------------------------------------# ALBLogBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}" BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: "AES256" BucketKeyEnabled: true PublicAccessBlockConfiguration: BlockPublicAcls: TRUE BlockPublicPolicy: TRUE IgnorePublicAcls: TRUE RestrictPublicBuckets: TRUE LifecycleConfiguration: Rules: - Id: !Sub "${SystemName}-${Environment}-rolling-alb-log-lifecycle" Status: Enabled ExpirationInDays: 400 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" logsBucketPolicy: Type: AWS::S3::BucketPolicy DependsOn: ALBLogBucket Properties: Bucket: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}" PolicyDocument: Statement: - Action: - 's3:PutObject' Effect: 'Allow' Resource: - Fn::Join: - '' - - "arn:aws:s3:::" - !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}" - "/*" Principal: AWS: '582318560864' - Action: - 's3:GetBucketAcl' Effect: 'Allow' Resource: Fn::Join: - '' - - "arn:aws:s3:::" - !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}" Principal: Service: 'logdelivery.elb.amazonaws.com' # ------------------------------------------------------------# # ECS Cluster # ------------------------------------------------------------# ECSCluster: Type: "AWS::ECS::Cluster" Properties: ClusterName: !Sub "${SystemName}-${Environment}-rolling-cluster" Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-cluster" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # ECS LogGroup # ------------------------------------------------------------# ECSLogGroup: Type: "AWS::Logs::LogGroup" Properties: LogGroupName: !Sub "${SystemName}-${Environment}-rolling-cluster-log" # ------------------------------------------------------------# # ECS Task Execution Role # ------------------------------------------------------------# ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${SystemName}-${Environment}-rolling-task-execution-role" Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-task-execution-role" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # ECS TaskDefinition # ------------------------------------------------------------# ECSTaskDefinition: Type: "AWS::ECS::TaskDefinition" Properties: Cpu: !Ref ECSTaskCPUUnit ExecutionRoleArn: !Ref ECSTaskExecutionRole Family: !Sub "${SystemName}-${Environment}-rolling-task-definition" Memory: !Ref ECSTaskMemory NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ContainerDefinitions: - Name: app-container Image: !Ref ECSImage LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref ECSLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: !Ref SystemName MemoryReservation: 128 PortMappings: - HostPort: 80 Protocol: tcp ContainerPort: 80 ReadonlyRootFilesystem: false Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-task-definition" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # ECS Service # ------------------------------------------------------------# ECSService: Type: AWS::ECS::Service DependsOn: ALBListener Properties: Cluster: !Ref ECSCluster DesiredCount: !Ref ECSTaskDesiredCount DeploymentConfiguration: DeploymentCircuitBreaker: Enable: true Rollback: true LaunchType: FARGATE LoadBalancers: - TargetGroupArn: !Ref TargetGroup ContainerPort: 80 ContainerName: !Sub "app-container" NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED SecurityGroups: - !Ref TaskSecurityGroup Subnets: - !Ref ECSSubnetId1 - !Ref ECSSubnetId2 PlatformVersion: 1.4.0 ServiceName: !Sub "${SystemName}-${Environment}-rolling-service" TaskDefinition: !Ref ECSTaskDefinition Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-service" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Auto Scaling Service # ------------------------------------------------------------# ServiceAutoScalingRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${SystemName}-${Environment}-rolling-autoscaling-role" AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: application-autoscaling.amazonaws.com Action: sts:AssumeRole Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-autoscaling-role" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" ServiceAutoScalingPolicy: Type: AWS::IAM::Policy Properties: PolicyName: !Sub "${SystemName}-${Environment}-rolling-autoscaling-policy" PolicyDocument: Statement: - Effect: Allow Action: - application-autoscaling:* - cloudwatch:DescribeAlarms - cloudwatch:PutMetricAlarm - ecs:DescribeServices - ecs:UpdateService Resource: '*' Roles: - !Ref ServiceAutoScalingRole ServiceScalingTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref TaskMinContainerCount MaxCapacity: !Ref TaskMaxContainerCount ResourceId: !Join - '/' - - 'service' - !Ref ECSCluster - !GetAtt ECSService.Name RoleARN: !GetAtt ServiceAutoScalingRole.Arn ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs DependsOn: - ECSService - ServiceAutoScalingRole ServiceScalingPolicyCPU: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Sub "${SystemName}-${Environment}-rolling-target-tracking-scaling-cpu" PolicyType: TargetTrackingScaling ScalingTargetId: !Ref ServiceScalingTarget TargetTrackingScalingPolicyConfiguration: TargetValue: !Ref ServiceScaleCpuTarget ScaleInCooldown: !Ref ServiceScaleInCooldown ScaleOutCooldown: !Ref ServiceScaleOutCooldown PredefinedMetricSpecification: PredefinedMetricType: ECSServiceAverageCPUUtilization DependsOn: ServiceScalingTarget
ECS構築のポイント
- ALBは特定のソースIPからのみアクセス可能な想定
- 変数ALBAllowInboundIPでセキュリティグループのインバウンドルールで許可するIPアドレスを指定
- ALBのアクセスログはS3に出力
- SSE-S3(AES256)で暗号化し、400日のライフサイクルを設定
- ECSサービスのAutoScalingはCPUの追跡ポリシーを設定
- 閾値は変数で設定可能。変数値のデフォルトには70%を設定
- Rollingアップデートではデプロイサーキットブレイカーを有効化
CI/CD環境構築
Rollingアップデート用CI/CDスタック作成
次のCFnテンプレートを使ってスタックを作成し、Rollingアップデート用のCI/CD環境を構築します。
cicd-rolling.yaml(クリックで展開)
AWSTemplateFormatVersion: 2010-09-09 Description: Rolling Update For ECS Fargate with GitHub Metadata: "AWS::CloudFormation::Interface": ParameterGroups: - Label: default: "System Configuration" Parameters: - Environment - SystemtName - Label: default: "VPC Configuration" Parameters: - VpcId - CodeBuildSubnetId1 - CodeBuildSubnetId2 - Label: default: "ECR・ECS Configuration" Parameters: - ECSClusterName - ECSServiceName - ECSTaskContainerName - ECRName - Label: default: "GitHub Configuration" Parameters: - GitHubOrganizationName - GitHubRepositoryName - GitHubBranchName Parameters: Environment: Default: dev Type: String SystemName: Default: system Type: String ECSClusterName: Default: system-dev-rolling-cluster Description : "CI/CD Deploy ECS Cluster" Type: String ECSServiceName: Default: system-dev-rolling-service Description : "CI/CD Deploy ECS Service" Type: String ECSTaskContainerName: Default: app-container Description : "CI/CD Deploy ECS Task Container" Type: String ECRName: Default: system-dev-repo Description : "ECR Repository Name" Type: String GitHubOrganizationName: Default: GitHubOrganizationName Description : "CI/CD GitHub Organization" Type: String GitHubRepositoryName: Default: GitHubRepositoryName Description : "CI/CD GitHub Repository" Type: String GitHubBranchName: Default: GitHubBranchName Description : "CI/CD GitHub Branch" Type: String VpcId: Default: vpc- Description : "VPC ID" Type: AWS::EC2::VPC::Id CodeBuildSubnetId1: Default: subnet-private1Id Description : "Private Subnet 1st" Type: AWS::EC2::Subnet::Id CodeBuildSubnetId2: Default: subnet-private2Id Description : "Private Subnet 2nd" Type: AWS::EC2::Subnet::Id Resources: # ------------------------------------------------------------# # Security Group # ------------------------------------------------------------# CodeBuildSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Rolling Update CodeBuild Security Group GroupName: !Sub "${SystemName}-${Environment}-rolling-code-build-sg" VpcId: !Ref VpcId SecurityGroupEgress: - IpProtocol: -1 CidrIp: 0.0.0.0/0 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-code-build-sg" - Key: Systemname Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # CICD Role # ------------------------------------------------------------# CodeBuildServiceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${SystemName}-${Environment}-rolling-cicd-build-role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-cicd-build-role" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" CodeBuildServicePolicy: Type: AWS::IAM::Policy DependsOn: CodeBuildServiceRole Properties: PolicyName: !Sub "${SystemName}-${Environment}-rolling-cicd-build-policy" PolicyDocument: Version: "2012-10-17" Statement: - Resource: "*" Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Resource: !Sub arn:aws:s3:::${RollingArtifactBucket}/* Effect: Allow Action: - s3:GetObject - s3:PutObject - s3:GetObjectVersion - s3:GetBucketAcl - s3:GetBucketLocation - Resource: "*" Effect: Allow Action: - ecr:GetAuthorizationToken - ecr:BatchCheckLayerAvailability - ecr:GetDownloadUrlForLayer - ecr:GetRepositoryPolicy - ecr:DescribeRepositories - ecr:ListImages - ecr:DescribeImages - ecr:BatchGetImage - ecr:InitiateLayerUpload - ecr:UploadLayerPart - ecr:CompleteLayerUpload - ecr:PutImage - Resource: "*" Effect: Allow Action: - ec2:CreateNetworkInterface - ec2:DescribeDhcpOptions - ec2:DescribeNetworkInterfaces - ec2:DeleteNetworkInterface - ec2:DescribeSubnets - ec2:DescribeSecurityGroups - ec2:DescribeVpcs - Resource: "*" Effect: Allow Action: - ec2:CreateNetworkInterface - ec2:DescribeDhcpOptions - ec2:DescribeNetworkInterfaces - ec2:DeleteNetworkInterface - ec2:DescribeSubnets - ec2:DescribeSecurityGroups - ec2:DescribeVpcs - ec2:CreateNetworkInterfacePermission - Resource: "*" Effect: Allow Action: - codestar-connections:UseConnection Roles: - !Ref CodeBuildServiceRole CodePipelineServiceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline-role" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-cicd-build-role" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" CodePipelineServicePolicy: Type: AWS::IAM::Policy DependsOn: CodePipelineServiceRole Properties: PolicyName: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline-policy" PolicyDocument: Version: 2012-10-17 Statement: - Resource: - !Sub arn:aws:s3:::${RollingArtifactBucket}/* Effect: Allow Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning - Resource: "*" Effect: Allow Action: - codecommit:GetRepository - codecommit:ListBranches - codecommit:GetUploadArchiveStatus - codecommit:UploadArchive - codecommit:CancelUploadArchive - codedeploy:CreateDeployment - codedeploy:GetApplication - codedeploy:GetApplicationRevision - codedeploy:GetDeployment - codedeploy:GetDeploymentConfig - codedeploy:RegisterApplicationRevision - codebuild:StartBuild - codebuild:StopBuild - codebuild:BatchGet* - codebuild:Get* - codebuild:List* - codecommit:GetBranch - codecommit:GetCommit - s3:* - ecs:* - elasticloadbalancing:* - autoscaling:* - iam:PassRole - codestar-connections:UseConnection Roles: - !Ref CodePipelineServiceRole # ------------------------------------------------------------# # Rolling Artifact S3 Bucket # ------------------------------------------------------------# RollingArtifactBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub "${SystemName}-${Environment}-rolling-cicd-artifact-${AWS::AccountId}" BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms BucketKeyEnabled: true PublicAccessBlockConfiguration: BlockPublicAcls: TRUE BlockPublicPolicy: TRUE IgnorePublicAcls: TRUE RestrictPublicBuckets: TRUE LifecycleConfiguration: Rules: - Id: !Sub "${SystemName}-${Environment}-rolling-cicd-build-artifact-lifecycle" Status: Enabled ExpirationInDays: 1827 Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-cicd-artifact-${AWS::AccountId}" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # GitHub Connection # ------------------------------------------------------------# GitHubConnection: Type: AWS::CodeStarConnections::Connection Properties: ConnectionName: !Sub "${SystemName}-${Environment}-rolling-cicd-github" ProviderType: GitHub Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-cicd-github" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Code Build LogGroup # ------------------------------------------------------------# CodeBuildLogGroup: Type: "AWS::Logs::LogGroup" Properties: LogGroupName: !Sub "${SystemName}-${Environment}-rolling-cicd-code-build" # ------------------------------------------------------------# # Code Build # ------------------------------------------------------------# CodeBuildProject: Type: AWS::CodeBuild::Project DependsOn: CodeBuildServicePolicy Properties: Name: !Sub "${SystemName}-${Environment}-rolling-cicd-code-build" Environment: PrivilegedMode: true ComputeType: BUILD_GENERAL1_SMALL Image: "aws/codebuild/amazonlinux2-x86_64-standard:3.0" Type: LINUX_CONTAINER EnvironmentVariables: - Name: AWS_REGION Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName} - Name: ECSTaskContainerName Value: !Ref ECSTaskContainerName Artifacts: Type: CODEPIPELINE Source: Type: CODEPIPELINE BuildSpec: | version: 0.2 env: variables: DOCKER_BUILDKIT: "1" phases: install: runtime-versions: docker: 19 pre_build: commands: - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) - echo Logging in to ECR - aws ecr --region ${AWS_REGION} get-login-password | docker login --username AWS --password-stdin https://${REPOSITORY_URI} - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) - IMAGE_TAG=${COMMIT_HASH:=latest} build: commands: - echo Build started on `date` - docker image build -t ${REPOSITORY_URI}:${COMMIT_HASH} . post_build: commands: - echo Build completed on `date` - echo Pushing the Docker images - docker image push ${REPOSITORY_URI}:${COMMIT_HASH} - printf '[{"name":"%s","imageUri":"%s"}]' ${ECSTaskContainerName} ${REPOSITORY_URI}:${COMMIT_HASH} > imagedefinitions.json artifacts: files: imagedefinitions.json ServiceRole: !Ref CodeBuildServiceRole Cache: Modes: - "LOCAL_DOCKER_LAYER_CACHE" - "LOCAL_SOURCE_CACHE" Type: "LOCAL" LogsConfig: CloudWatchLogs: Status: "ENABLED" GroupName: !Ref CodeBuildLogGroup Visibility: "PRIVATE" TimeoutInMinutes: 60 QueuedTimeoutInMinutes: 480 VpcConfig: SecurityGroupIds: - !Ref CodeBuildSecurityGroup Subnets: - !Ref CodeBuildSubnetId1 - !Ref CodeBuildSubnetId2 VpcId: !Ref VpcId Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-cicd-build" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}" # ------------------------------------------------------------# # Code Pipeline # ------------------------------------------------------------# Pipeline: Type: AWS::CodePipeline::Pipeline DependsOn: CodePipelineServicePolicy Properties: RoleArn: !GetAtt CodePipelineServiceRole.Arn Name: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline" ArtifactStore: Type: S3 Location: !Ref RollingArtifactBucket Stages: - Name: Source Actions: - Name: Source ActionTypeId: Category: Source Owner: AWS Version: 1 Provider: CodeStarSourceConnection Configuration: FullRepositoryId: !Sub - ${GitHubOrganizationName}/${GitHubRepositoryName} - GitHubOrganizationName: !Ref GitHubOrganizationName GitHubRepositoryName: !Ref GitHubRepositoryName ConnectionArn: !Ref GitHubConnection BranchName: !Ref GitHubBranchName OutputArtifactFormat: CODEBUILD_CLONE_REF RunOrder: 1 OutputArtifacts: - Name: SourceArtifact - Name: Build Actions: - Name: Build ActionTypeId: Category: Build Owner: AWS Version: 1 Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildProject RunOrder: 1 InputArtifacts: - Name: SourceArtifact OutputArtifacts: - Name: BuildArtifact - Name: Deploy Actions: - Name: Deploy ActionTypeId: Category: Deploy Owner: AWS Version: 1 Provider: ECS Configuration: ClusterName: !Ref ECSClusterName ServiceName: !Ref ECSServiceName FileName: imagedefinitions.json RunOrder: 1 InputArtifacts: - Name: BuildArtifact Tags: - Key: Name Value: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline" - Key: SystemName Value: !Sub "${SystemName}" - Key: Environment Value: !Sub "${Environment}"
CI/CD環境構築のポイント
- ソースアクションにはGitHubバージョン2を指定
- このため、初回のパイプラインは必ず失敗
- コンソール画面での接続設定後、ソースアクションの実行が可能になる
- アーティファクトはS3に出力
- SSE-S3(AES256)で暗号化し、400日のライフサイクルを設定
動作確認
それでは構築の動作確認をします。
### CFn構築
下記の順番に実行します。
- vpc.yaml
- ecr-rolling.yaml
- 構築後、latestタグを付与したイメージをpushすること
- ecs-rolling.yaml
- vpcの値に合わせてパラメータファイルを更新すること
- cicd-rolling.yaml
- vpc・ecsの値に合わせてパラメータファイルを更新すること
AWS CLIで下記のコマンドを実行することで、スタックを構築します。
aws cloudformation create-stack \ --stack-name スタック名 \ --template-body file://./テンプレート名 \ --capabilities CAPABILITY_NAMED_IAM \ --parameters file://./parameters/パラメータ名 \ --tags Key=Environment,Value=<Environmentに設定する変数値> \ --tags Key=SystemName,Value=<SystemNameに設定する変数値>
GitHubのリポジトリ構築
Rollingアップデート用のリポジトリを作成し、サンプルソースのGitHub/
フォルダの内容をアップロードします。
AWSとGitHubとの接続
作成したCI/CDパイプラインはGitHubとの接続ができずに初回実行は失敗しています。
コンソール画面からGitHubとの接続の設定を行います。
新しいアプリをインストールする、をクリックします。
GitHubの画面でさきほど作成したリポジトリを選択してSaveします
接続のステータスが利用可能になっていることを確認します。
Rollingアップデートの実行
index.htmlを次のように更新し、GitHubのmainブランチにpushします。
index.html(クリックで展開)
<!DOCTYPE html> <head> <meta charset="utf-8" /> <title>testページ</title> </head> <body> <h1>Rolling Update Demo</h1> </body> </html>
pushを契機としてパイプラインが起動されます。
しばらく待つと、ビルドも完了します。
ECRを確認すると、さきほどpushしたGitHubのコミットIDをタグとする新たなイメージが作成されています。
Rollingアップデートの途中は、新たなタスク定義のリビジョンが作られて、そこからタスクが起動されます。
その結果、一時的に新旧2つのバージョンのタスクが存在します。
しばらく待つとRollingアップデートが完了し、古いテスク定義を持つタスクは停止、削除されます。
ブラウザからALBのエンドポイントにアクセスすると、GitHubにpushしたindex.htmlの値に表示が変わっており、更新が完了していることが確認できます。
作成リソースの削除
作成時とは逆の順番で、CloudFormationスタックを削除し、リソースを削除してください。
最後に
ECS Fargateのデプロイについて、Rollingアップデート環境をCFnで構築する方法を紹介しました。
試したことのない方はぜひ一度やってみてください!
本ブログがどなたかのお役に立てば幸いです。